package com.googlecode.curvedmetromaps.core.presentation;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.io.OutputStreamWriter;
import org.apache.batik.svggen.SVGGraphics2D;
import org.apache.batik.svggen.SVGGraphics2DIOException;
import org.apache.batik.dom.GenericDOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DOMImplementation;

import com.googlecode.curvedmetromaps.core.geometry.BasicGeometry;
import com.googlecode.curvedmetromaps.core.model.MetroEdge;
import com.googlecode.curvedmetromaps.core.model.MetroLine;
import com.googlecode.curvedmetromaps.core.model.MetroMap;
import com.googlecode.curvedmetromaps.core.model.MetroStation;

public class SvgGenerator {
	
	private MetroMap map;

	/** Variables to scale model to canvas coordinates */
	private float		canvasMinX, canvasMaxY, canvasScale;
	
	private final static float xCoordinateRange = 1000;
	private final static float canvasMargin = 10;
	
	private float stationOuterSize = 6f; 
	private float stationInnerSize = 4f;
	
	public SvgGenerator(MetroMap map) {
		this.map = map;
	}
	
	public void write(String path) {
	    // Get a DOMImplementation.
	    DOMImplementation domImpl =
	      GenericDOMImplementation.getDOMImplementation();

	    // Create an instance of org.w3c.dom.Document.
	    String svgNS = "http://www.w3.org/2000/svg";
	    Document document = domImpl.createDocument(svgNS, "svg", null);

	    // Create an instance of the SVG Generator.
	    SVGGraphics2D svgGenerator = new SVGGraphics2D(document);
	    // Update model scaling and get canvas size.
    	Dimension d = updateBoundingBox();

	    // Ask the test to render into the SVG Graphics2D implementation.
	    paint(svgGenerator);

	    // Open file stream
	    Writer out = null;
		try {
			File file = new File(path);
			FileOutputStream fs = new FileOutputStream(file);
			out = new OutputStreamWriter(fs, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		
		// Stream SVG to output
	    boolean useCSS = true;
	    try {
	    	// Set SVG size
	    	svgGenerator.setSVGCanvasSize(d);
	    	
			svgGenerator.stream(out, useCSS);
			System.out.print("");
		} catch (SVGGraphics2DIOException e) {
			// Failed to write to output file.
			e.printStackTrace();
		}
	}

	private float getCanvasX(float modelX) {
		return canvasMargin + ((modelX - canvasMinX) * canvasScale);
	}

	private float getCanvasY(float modelY) {
		return canvasMargin + ((canvasMaxY - modelY) * canvasScale);
	}

	private void paint(Graphics2D g2d) {
	    drawMetroLines(g2d);
	    drawMetroStations(g2d);
	}
	
	private Dimension updateBoundingBox() {
		float[] boundingBox = BasicGeometry.getBoundingBox(map.stations);
		canvasMinX = boundingBox[0];
		canvasMaxY = boundingBox[3];

		float w = boundingBox[2] - boundingBox[0];
		float h = boundingBox[3] - boundingBox[1];
		canvasScale = w > 0 ? xCoordinateRange / w : 1;
		
		double canvasW = (2 * canvasMargin) + xCoordinateRange;
		double canvasH = (2 * canvasMargin) + Math.ceil(canvasScale * h);
    	Dimension d = new Dimension();
    	// It's great that setSize has double parameters, but the values are internally cast to int.
    	d.setSize(canvasW, canvasH);
    	return d;
	}
	
	private void drawMetroStations(Graphics2D g2d) {
		Color black = new Color(0, 0, 0);
		Color white = new Color(0xFF, 0xFF, 0xFF);
		
		float inAdd = -0.5f * stationInnerSize;
		float outAdd = -0.5f * stationOuterSize;
		
		for (MetroStation m : map.stations) {
			float cX = getCanvasX(m.x);
			float cY = getCanvasY(m.y);
			// Black circle
			Ellipse2D outerCircle = new Ellipse2D.Float(cX+outAdd, cY+outAdd, stationOuterSize, stationOuterSize);
			Ellipse2D innerCircle = new Ellipse2D.Float(cX+inAdd, cY+inAdd, stationInnerSize, stationInnerSize);
			g2d.setPaint(black);
			g2d.fill(outerCircle);
			g2d.setPaint(white);
			g2d.fill(innerCircle);
		}
	}

	/**
	 * Draws metro lines as using quadratic Bézier splines.
	 * @param g2d Canvas to draw upon.
	 */
	private void drawMetroLines(Graphics2D g2d) {
		for (MetroLine line : map.lines) {
			Path2D path = new Path2D.Float();
			for (int i = 1; i < line.stations.length; i++) {
				MetroStation m1 = line.stations[i - 1];
				MetroStation m2 = line.stations[i];
				MetroEdge e = map.getEdge(m1, m2);
				
				if (e != null) {
					if (i == 1)
						path.moveTo(getCanvasX(m1.x), getCanvasY(m1.y));
					
					path.quadTo(getCanvasX(e.cpX), getCanvasY(e.cpY), getCanvasX(m2.x), getCanvasY(m2.y));
				}
			}
			
			// Set color
			int color = line.color != 0 ? line.color : Colors.colors[line.id];
			g2d.setColor(new Color(color));
			
			// Draw quadratic spline
			g2d.setStroke(new BasicStroke(4f));
			g2d.draw(path);
		}
	}	
}
